5.05. Работа с БД и ORM
Работа с БД и ORM
ORM и Entity Framework Core
Что такое ORM
Настройка контекста, миграции
★ Работа с БД – одна из ключевых задач при разработке приложений на языке C#. Платформа предоставляет богатый набор инструментов, позволяющих взаимодействовать с реляционными базами данных различными способами, в зависимости от требований проекта, уровня абстракции, производительности и удобства использования:
- ADO.NET – низкоуровневый, гибкий и производительный способ работы с БД через SQL-запросы;
- LINQ – мощный механизм запросов, встроенный в C#, позволяющий работать с данными декларативно;
- LINQ to SQL – легковесный ORM для работы с Microsoft SQL Server;
- Entity Framework (EF) / EF Core – полноценный ORM, поддерживающий миграции, отслеживание изменений и работу с множеством СУБД;
- LINQ to DB – высокопроизводительный ORM, сочетающий гибкость ADO.NET и удобство LINQ.
★ Давайте для начала определим общий алгоритм работы с БД.
- Создать или выбрать БД, определить СУБД, таблицы, поля, связи и ограничения.
- Настроить параметры подключения – указать адрес сервера, имя базы данных, логин, пароль и другие параметры, если требуется. Эти параметры обычно хранятся в конфигурационном файле (
appsettings.json, App.config или Web.config) или внедряются через зависимости. - Установка подключения к базе данных – нужно открыть соединение с БД, проверить состояние перед началом работы. В ADO.NET это управляется вручную, в отличие от прочих фреймворков.
- Формирование и выполнение запросов – нужно подготовить SQL-запрос или LINQ-выражение, выбрав подходящий метод выполнения – чтение (SELECT), изменение (INSERT, UPDATE, DELETE), или получение одного значения (ExecuteScalar).
- Получить и обработать результаты – если запрос возвращает данные – извлечь их и преобразовать в нужный формат (объекты, коллекции, строки и т.д.), обработать возможные ошибки и исключения.
- Сохранение изменений – если мы добавляли, изменяли или удаляли данные, нужно убедиться в сохранении, при необходимости использовать транзакции.
- Закрыть соединение, чтобы освободить ресурсы.
Этот алгоритм универсален для всех, разница лишь в том, как именно реализуется каждый шаг:
- в ADO.NET всё контролируется вручную;
- в LINQ to SQL и EF логика абстрагирована, и многое делается автоматически;
- в LINQ to DB – баланс между производительностью и удобством.
Как подключиться к базе данных в С#
В C# подключение к базе данных происходит с помощью ADO.NET, который предоставляет богатый набор классов для доступа к базам данных. Для подключения создается объект Connection, указывается строка подключения, и затем с помощью объектов Command выполняются запросы SQL или хранящиеся процедуры. В конце подключение закрывается и обрабатываются любые возникшие исключения.
★ ADO.NET (ActiveX Data Objects .NET) – набор классов в .NET Framework, которые позволяют напрямую взаимодействовать с базами данных через SQL-запросы. Это безопасный, гибкий и производительный способ работы с БД без абстракций ORM. ADO.NET не создаёт объектов из таблиц автоматически – разработчик самостоятельно управляет подключениями, командами, данными и их обработкой.
Основные компоненты ADO.NET:
| Компонент | Описание |
|---|---|
Connection | Устанавливает соединение с базой данных |
Command | Выполняет SQL-запрос или хранимую процедуру |
DataReader | Обеспечивает последовательное чтение данных в режиме только для чтения |
DataAdapter | Заполняет DataSet данными и позволяет синхронизировать изменения с источником |
DataSet | Кэшированная, отключенная от источника копия данных, поддерживающая таблицы, связи и ограничения |
Transaction | Группирует команды в единую атомарную операцию (ACID) |
Каждый тип СУБД имеет свой провайдер:
| Провайдер | СУБД |
|---|---|
System.Data.SqlClient | Microsoft SQL Server |
System.Data.OleDb | Источники через OLE DB (например, Access, старые версии Oracle) |
System.Data.Odbc | Любые СУБД с ODBC-драйвером |
MySql.Data.MySqlClient | MySQL |
Npgsql | PostgreSQL |
Примеры использования ADO.NET
Шаг 1: подключение к БД:
string connectionString = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;";
using (SqlConnection connection = new SqlConnection(connectionString)) {
connection.Open();
Console.WriteLine("Подключено к БД");
}
Шаг 2а: Выполнение запроса на выборку (SELECT):
string query = "SELECT Id, Name FROM Users";
using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlCommand command = new SqlCommand(query, connection);
connection.Open();
using (SqlDataReader reader = command.ExecuteReader()) {
while (reader.Read()) {
int id = reader.GetInt32(0); // Индекс колонки
string name = reader.GetString(1);
Console.WriteLine($"ID: {id}, Имя: {name}");
}
}
}
Шаг 2б: выполнение запроса на изменение (INSERT, UPDATE, DELETE):
string query = "INSERT INTO Users (Name, Email) VALUES (@name, @email)";
using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@name", "Alice");
command.Parameters.AddWithValue("@email", "alice@example.com");
connection.Open();
int rowsAffected = command.ExecuteNonQuery();
Console.WriteLine($"{rowsAffected} записей добавлено.");
}
Шаг 2в: использование параметров (защита от SQL-инъекций):
command.Parameters.Add("@age", SqlDbType.Int).Value = 30;
// или
command.Parameters.AddWithValue("@age", 30);
Шаг 2г: получение одного значения (ExecuteScalar):
string query = "SELECT COUNT(*) FROM Users";
SqlCommand command = new SqlCommand(query, connection);
object result = command.ExecuteScalar();
int count = Convert.ToInt32(result);
Console.WriteLine($"Всего пользователей: {count}");
Шаг 2д: работа с DataSet и DataAdapter (загрузка данных в память):
string query = "SELECT * FROM Products";
DataSet ds = new DataSet();
using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlDataAdapter adapter = new SqlDataAdapter(query, connection);
adapter.Fill(ds, "Products");
foreach (DataRow row in ds.Tables["Products"].Rows) {
Console.WriteLine(row["ProductName"]);
}
}
Шаг 2е: асинхронное выполнение запросов:
async Task<int> InsertUserAsync(string name, string email) {
string query = "INSERT INTO Users (Name, Email) VALUES (@name, @email)";
using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@name", name);
command.Parameters.AddWithValue("@email", email);
await connection.OpenAsync();
return await command.ExecuteNonQueryAsync();
}
}
★ ORM (Object Relational Mapping) – технология, которая связывает объектно-ориентированную модель программы с реляционной моделью базы данных. Это упрощает работу с БД через ОПП, избавляет от необходимости писать SQL-запросы вручную, обеспечивает типобезопасность и поддержку LINQ, а также автоматизирует CRUD-операции.
★ LINQ (Language Integrated Query) – это встроенный в C# механизм запросов к различным источникам данных: коллекциям, массивам, XML, БД и т.д.
Пример простого LINQ-запроса:
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
Когда используется вместе с ORM, LINQ-запросы преобразуются в SQL на лету и выполняются на СУБД.
★ LINQ to SQL – легковесный ORM, позволяющий работать с SQL Server через LINQ-запросы.
Ключевые компоненты:
| Компонент | Описание |
|---|---|
DataContext | Основной класс LINQ to SQL, обеспечивающий соединение с базой данных и управление операциями чтения/записи |
[Table] | Атрибут, сопоставляющий класс с таблицей в базе данных (указывается на уровне класса) |
[Column] | Атрибут, сопоставляющий свойство класса с колонкой в таблице (поддерживает указание имени, типа, ключа и др.) |
GetTable<T>() | Метод DataContext, возвращающий объект ITable<T>, представляющий таблицу как коллекцию сущностей для выполнения LINQ-запросов |
Пример использования LINQ to SQL:
Шаг 1: определение класса, соответствующего таблице:
[Table(Name = "Customers")]
public class Customer {
[Column(IsPrimaryKey = true)]
public int Id { get; set; }
[Column]
public string Name { get; set; }
}
Шаг 2: работа с данными:
string connectionString = "...";
using (DataContext db = new DataContext(connectionString)) {
var customers = db.GetTable<Customer>();
// Выборка
var query = from c in customers
where c.Name.StartsWith("A")
select c;
foreach (var c in query) {
Console.WriteLine(c.Name);
}
// Добавление
Customer newCustomer = new Customer { Name = "Alice" };
customers.InsertOnSubmit(newCustomer);
db.SubmitChanges();
}
★ LINQ to DB – высокопроизводительная альтернатива EF и LINQ to SQL. Это гибрид ORM и генератора SQL-запросов. У него очень высокая производительность, поддержка множества СУБД, полная интеграция с LINQ, минимальное количество абстракций.
Основные классы:
| Компонент | Описание |
|---|---|
DataConnection | Базовый класс в LINQ to DB, предоставляющий соединение с базой данных и методы для выполнения запросов |
[Table] | Атрибут, сопоставляющий класс с таблицей в базе данных (указывается на уровне класса) |
[Column] | Атрибут, сопоставляющий свойство или поле класса с колонкой в таблице |
[PrimaryKey] | Атрибут, указывающий, что свойство представляет первичный ключ таблицы |
Пример работы с LINQ to DB
Шаг 1: Определение модели:
[Table(Name = "Users")]
public class User {
[PrimaryKey, Identity]
public int Id { get; set; }
[Column]
public string Name { get; set; }
}
Шаг 2: Контекст БД:
public class MyDb : DataConnection {
public ITable<User> Users => GetTable<User>();
public MyDb() : base("MyConnectionString") {}
}
Шаг 3: Использование:
using (var db = new MyDb()) {
// Добавление
db.Users.Insert(() => new User { Name = "Bob" });
// Запрос
var users = db.Users.Where(u => u.Name.StartsWith("B")).ToList();
// Обновление
db.Users.Where(u => u.Id == 1)
.Set(u => u.Name, "Alice")
.Update();
// Удаление
db.Users.Delete(u => u.Id == 1);
}
В реляционных БД можно использовать определенные типы связей – один ко многим, многие ко многим и один к одному.
One-to-Many (Один ко многим) – самый распространённый тип связи. Один автор может написать много книг, но каждая книга имеет только одного автора. В EF такая связь реализуется через навигационные свойства и внешние ключи.
Many-to-Many (многие ко многим) – к примеру, студент может посещать много курсов, курс может содержать много студентов. В реляционных БД такая связь реализуется через соединительную таблицу. Соединительная таблица может содержать дополнительные поля (например, дату зачисления), а запросы с такими связями могут создавать «картезианский взрыв» (много дублирующихся данных).
One-to-One (один к одному) – пользователь имеет один профиль, профиль принадлежит одному пользователю. Используется для разделения больших таблиц, может быть и необязательной связью, а также внешний ключ может находиться в любой из таблиц.
★ Entity Framework — это ORM-фреймворк, созданный Microsoft. Он позволяет разработчикам работать с базами данных с помощью принципов ООП вместо написания необработанных SQL запросов. Entity Framework автоматически сопоставляет таблицы базы данных с классами и даёт такие функции, как поддержка LINQ, отслеживание изменений и CRUD операции. Он упрощает доступ и изменение баз данных в приложениях C#.
Основные компоненты:
| Компонент | Описание |
|---|---|
DbContext | Основной класс Entity Framework, представляющий сессию с базой данных; управляет подключением, отслеживанием сущностей и сохранением изменений |
DbSet<TEntity> | Представляет коллекцию сущностей типа TEntity, соответствующую таблице в базе данных; используется для запросов и манипуляций с данными |
OnModelCreating() | Метод, который можно переопределить для настройки модели с помощью Fluent API (например, связи, индексы, имена таблиц) |
SaveChanges() | Сохраняет все изменения, отслеживаемые контекстом, в базу данных; выполняет INSERT, UPDATE, DELETE операции |
Migrations | Механизм управления версиями схемы базы данных; позволяет создавать и применять миграции на основе изменений в модели (Add-Migration, Update-Database) |
LINQ | Язык интегрированных запросов, используемый для построения типобезопасных запросов к данным через DbSet, которые транслируются в SQL |
Давайте разберём некоторые компоненты EF и их назначение.
EF состоит из нескольких ключевых компонентов, каждый из которых играет свою роль в работе с базой данных.
- EF Designer - графический инструмент для создания модели данных. Позволяет визуально проектировать модель (например, в подходе Model First). Автоматически генерирует классы и контекст на основе диаграммы. Разработчик сначала создаёт диаграмму в Visual Studio, а EF Designer генерирует SQL-скрипты для создания БД и классы для работы.
- EF Tools - инструменты командной строки и интерфейса для управления миграциями. Нужно для добавления, применения, отката миграций, создания скриптов для БД.
- Migrations - механизм управления изменениями структуры базы данных. Нужен для последовательного обновления БД, поддержки версионирования и отката изменений. Каждая миграция представляет собой шаг в эволюции базы данных, а EF хранит историю миграций в таблице
__EFMigrationsHistory. - DbContext - центральный компонент EF, который управляет взаимодействием с базой данных. Он предоставляет доступ к таблицам через свойства DbSet, управляет транзакциями и состоянием объектов. Разработчик создаёт класс, наследующий DbContext, и определяет в нём свойства DbSet.
- LINQ - язык запросов. LINQ является важной частью, так как предоставляет готовый набор решения для «общения» с базами данных.
★ Пример работы с EF:
Шаг 1: Создание модели:
public class Product {
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Шаг 2: Контекст БД:
public class AppDbContext : DbContext {
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseSqlServer("Server=...;Database=MyDB;Trusted_Connection=True;");
}
}
Шаг 3: Использование:
using (var db = new AppDbContext()) {
// Добавление
var product = new Product { Name = "Laptop", Price = 1200 };
db.Products.Add(product);
db.SaveChanges();
// Запрос
var expensiveProducts = db.Products.Where(p => p.Price > 1000).ToList();
// Обновление
product.Price = 1100;
db.SaveChanges();
// Удаление
db.Products.Remove(product);
db.SaveChanges();
}
★ Модели данных в EF представляют собой классы C#, которые описывают структуру таблиц в БД. Эти классы используются для работы с данными через ORM.
Сначала нужно опеределить классы (сущности). Каждый класс соответствует таблице в БД, а свойства класса соответствуют столбцам таблицы.
Пример:
public class User
{
public int Id { get; set; } // Первичный ключ
public string Name { get; set; }
public int Age { get; set; }
}
public class Order
{
public int Id { get; set; }
public string ProductName { get; set; }
public int UserId { get; set; } // Внешний ключ
public User User { get; set; } // Навигационное свойство
}
Затем нужно настроить контекст (DbContext) - создать класс, наследующий DbContext. Он управляет взаимодействием с базой данных.
Для каждой сущности нужно добавить свойства DbSet<T>:
public class ApplicationContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("YourConnectionStringHere");
}
}
Потом добавляются атрибуты или Fluent API (опционально), при помощи аннотаций - для настройки маппинга. Затем производится миграция для создания базы данных на основе модели.
★ Атрибуты бывают разных видов.
Атрибуты для первичного ключа - [Key], он указывает, что свойство является первичным ключом (PRIMARY KEY).
[Key]
public int Id { get; set; }
Атрибуты для обязательных или необязательных полей. [Required] равнозначен NOT NULL - обозначает, что поле обязательно для заполнения.
Если без [Required], значит поле может быть NULL. В таком случае, если тип nullable, нужно добавить вопросительный знак.
Пример:
[Required]
public string Name { get; set; }
public string? Address { get; set; } // NULL в SQL
Атрибуты для длины и типа данных:
[MaxLength(100)]равнозначенNVARCHAR(100)- ограничение длины строки;[Column(TypeName = “varchar(50)”)]равнозначенVARCHAR(50)- точное указание типа SQL;[Precision(18, 2)]равнозначенDECIMAL(18,2)- для чисел с фиксированной точностью. Пример:
[MaxLength(100)]
public string Email { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Salary { get; set; }
Атрибуты для связей между таблицами:
[ForeignKey(“AuthorId”)]равнозначен FOREIGN KEY (AuthorId), указывает свойство-внешний ключ;[InverseProperty(“Books”)]- навигационное поле для связи.
Пример (один-ко-многим):
public class Author
{
[Key]
public int Id { get; set; }
public List<Book> Books { get; set; } // Навигационное свойство
}
public class Book
{
[Key]
public int Id { get; set; }
public int AuthorId { get; set; } // Внешний ключ
[ForeignKey("AuthorId")]
public Author Author { get; set; } // Навигационное свойство
}
Атрибуты для индексов:
[Index]равнозначен CREATE INDEX - создаёт некластеризованный индекс;[Index(IsUnique = true)]равнозначен CREATE UNIQUE INDEX - уникальный индекс.
ADO.NET используется в качестве фундамента для работы с базой данных в Entity Framework. Однако EF абстрагирует большинство низкоуровневых операций ADO.NET, предоставляя более удобный интерфейс.
ADO.NET — это низкоуровневый подход, предоставляющий прямой доступ к базе данных через объекты, такие как SqlConnection, SqlCommand, SqlDataReader и т.д., а разработчик вручную пишет SQL-запросы и управляет подключением. Создаётся переменная, равная новому экземпляру класса SqlConnection с указанием базы данных. Потом вызывается метод Open, а затем уже создаются переменные для SqlCommand с текстом запроса и так далее.
Вот пример:
using (var connection = new SqlConnection("YourConnectionStringHere"))
{
connection.Open();
var command = new SqlCommand("SELECT * FROM Users", connection);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine(reader["Name"]);
}
}
}
EF же высокоуровневый подход, использует ADO.NET «под капотом» для выполнения SQL-запросов. Разработчик работает с объектами (классами) и LINQ-запросами, а EF автоматически их преобразует в SQL.
★ Миграции
Миграции в Entity Framework выполняются через команды в консоли диспетчера пакетов (Package Manager Console) или терминале. Вот пошаговый процесс.
- Настройка проекта - нужно подключить необходимые пакеты NuGet:
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Создание модели. Нужно определить классы, которые будут представлять таблицы в базе данных, к примеру:
класс User:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
класс ApplicationContext:
public class ApplicationContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("YourConnectionStringHere");
}
}
- Добавление миграции. В консоли диспетчера пакетов нужно выполнить команду для добавления миграции:
Add-Migration InitialCreate
Эта команда создаст файл миграции в папке Migrations. В нём будет описано, как изменить базу данных (например, создать таблицу Users).
- Применение миграции. Выполните команду для применения миграции к БД:
Update-Database
База данных будет обновлена в соответствии с изменениями.
- Откат миграции. Если нужно откатить последнюю миграцию:
Remove-Migration
Или откатить все миграции до определённой:
Update-Database LastGoodMigrationName
★ Работа с EF через CLI
Миграции можно выполнять как через консоль диспетчера пакетов, так и через CLI.
Установка:
dotnet tool install --global dotnet-ef
Обновление:
dotnet tool update --global dotnet-ef
Добавление миграции:
dotnet ef migrations add <MigrationName>
<MigrationName> — имя миграции (например, InitialCreate, AddUsersTable).
Эта команда создает файл миграции в папке Migrations вашего проекта.
Пример:
dotnet ef migrations add AddUsersTable
Применение миграции к базе данных:
dotnet ef database update
Применение конкретной миграции:
dotnet ef database update <MigrationName>
Откат миграции:
dotnet ef migrations remove
Эта команда удаляет последнюю миграцию из папки Migrations и отменяет её применение.
Чтобы увидеть список всех миграций:
dotnet ef migrations list
Для удаления базы данных:
dotnet ef database drop
Можно также сгенерировать SQL-скрипт:
dotnet ef migrations script
…и создать модель из существующей базы данных:
dotnet ef dbcontext scaffold
CLI особенно удобен для автоматизации процессов и работы в кроссплатформенных средах.
★ Как создавать модели через Model First?
Проектирование Model First в EF через Visual Studio
- Создание нового проекта.
- Добавление модели ADO.NET Entity Data Model:
- ПКМ на проекте - Add - New Item
- выбрать ADO.NET Entity Data Model
- выбрать Empty EF Designer Model.
Модель будет иметь расширение .edmx
- Проектирование модели:
- откроется графический редактор (EF Designer);
- нужно будет добавить сущности (User, Order) через панель инструментов;
- определить свойства сущностей (Id, Name, Age);
- настроить связи между сущностями (например, один-ко-многим).
- Генерация кода. После завершения проектирования, EF автоматически сгенерирует классы для каждой сущности и контекст (DbContext) для работы с БД.
- Создание БД:
- в свойствах модели нужно выбрать, какую СУБД использовать (например, SQL Server);
- нажать на генерацию (Generate Database from Model);
- EF создаст SQL-скрипт для создания БД;
- этот скрипт нужно выполнить в СУБД.
- Использование модели. После этого можно использовать сгенерированные классы в коде.
В коде использование идёт по принципу:
объявляется переменная, равная новому экземпляру класса контекста базы данных:
var context = new DbContext
объявляется переменная, равная новому экземпляру класса таблицы:
var user = new User { Name = "Alice", Age = 30 };
управление происходит через формат <контекст>.<таблица>.<метод/свойство>:
context.Users.Add(user);
context.SaveChanges();
★ Альтернатива EF
Для сложных запросов используется "сырой" SQL через метод context.Database.ExecuteSqlRaw().
Dapper - микро-ORM, который фокусируется на производительности.
В отличие от EF, основного ORM для .NET, интегрированного с экосистемой Microsoft, Dapper имеет минимальную абстракцию и высокую скорость выполнения запросов. Его можно встретить в проектах, где важна производительность и контроль над SQL.
LINQ в EF Core
Отслеживание изменений (Change Tracking)
Навигационные свойства
Ленивая загрузка (Lazy Loading), явная, Eager Loading
Постраничный вывод
Skip, Take в LINQ
Реализация пагинации
★ Методы при работе с БД
| Метод/свойство LINQ (EF) | SQL-эквивалент | Описание |
|---|---|---|
.Where() | WHERE | Фильтрация записей по условию. |
.Select() | SELECT (проекция) | Выбор конкретных полей или преобразование данных. |
.OrderBy() | ORDER BY ... ASC | Сортировка результатов по возрастанию. |
.OrderByDescending() | ORDER BY ... DESC | Сортировка результатов по убыванию. |
.ThenBy() / .ThenByDescending() | ORDER BY x, y | Дополнительная сортировка после основной. |
.First() / .FirstOrDefault() | SELECT TOP 1 ... | Получение первой записи; First() — с исключением при отсутствии, FirstOrDefault() — возвращает null. |
.Single() / .SingleOrDefault() | SELECT TOP 2 ... (с проверкой количества) | Получение единственной записи; Single() ожидает ровно один результат, иначе — исключение. |
.Count() | COUNT(*) | Подсчёт количества записей, удовлетворяющих условию. |
.Any() | EXISTS | Проверка наличия хотя бы одной записи, удовлетворяющей условию. |
.All() | NOT EXISTS (с условием NOT) | Проверка, что все записи в наборе удовлетворяют заданному условию. |
.Sum() / .Average() | SUM() / AVG() | Вычисление суммы или среднего значения числовой колонки. |
.Min() / .Max() | MIN() / MAX() | Получение минимального или максимального значения. |
.Skip() / .Take() | OFFSET ... FETCH NEXT ... ROWS ONLY | Реализация пагинации: пропуск первых N записей и выбор следующих M. |
.Include() | JOIN (жадная загрузка) | Загрузка связанных сущностей (навигационных свойств). |
.GroupBy() | GROUP BY | Группировка записей по ключу с возможностью агрегации. |
.Join() | INNER JOIN | Внутреннее соединение двух коллекций по ключу. |
.Distinct() | DISTINCT | Удаление дублирующихся записей из результата. |
.FromSqlRaw() | Прямой SQL-запрос | Выполнение "сырого" SQL-запроса напрямую к базе данных. |
ToListAsync() SaveChangesAsync() | SELECT * FROM Table INSERT/UPDATE/DELETE (асинхронный) | Асинхронное выполнение операций чтения и сохранения изменений. |
ToList() ToArray() ToDictionary() | SELECT * FROM Table | Материализация запроса: выполнение в БД и загрузка результатов в память. |
BeginTransaction() Database.BeginTransaction() Commit() Rollback() | BEGIN TRANSACTION COMMIT ROLLBACK | Управление транзакциями: начало, фиксация, откат. |
ExecuteProc() FromSqlRaw() | EXEC ProcedureName | Вызов хранимых процедур. |
AddRange() RemoveRange() | INSERT INTO ... VALUES (...) DELETE FROM ... WHERE Id IN (...) | Массовое добавление или удаление сущностей. |
★ Стратегии загрузки данных
Есть три стратегии загрузки связанных данных в EF - ленивая, жадная и явная.
Представим, что у нас есть класс User (пользователь), класс Order (заказ). Каждый User может иметь много Order. Это связанные таблицы. Как загрузить пользователя и его заказы?
Ленивая загрузка (Lazy Loading) - подход, при котором связанные данные (например, заказы пользователя) загружаются автоматически только при первом обращении к навигационному свойству.
Мы запрашиваем пользователя:
var user = db.Users.First(); // SELECT * FROM Users LIMIT 1
Пока мы не обратились к user.Orders, заказы не загружаются.
Но как только мы пишем:
var orders = user.Orders; // SELECT * FROM Orders WHERE UserId = 1
EF самостоятельно выполняет дополнительный запрос в БД.
При таком подходе не нужно заранее думать, какие данные нам понадобятся. Однако возникает проблема, называемая N+1 - если у нас 100 пользователей, EF сделает 101 запрос (1 для пользователей + 100 для заказов).
Запросил пользователя → Потом запросил заказы → Получил N+1.
EF использует прокси-классы (динамически создаваемые наследники классов), которые переопределяют навигационные свойства. При обращении к свойству, например, user.Orders, прокси выполняет SQL-запрос на лету.
Для этого нужно установить Microsoft.EntityFrameworkCore.Proxies:
dotnet add package Microsoft.EntityFrameworkCore.Proxies
Затем понадобится включить прокси в DbContext:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLazyLoadingProxies() // Включаем ленивую загрузку
.UseSqlServer("Your_Connection_String");
}
Навигационные свойства должны быть виртуальными (virtual):
public class User
{
public int Id { get; set; }
public virtual ICollection<Order> Orders { get; set; } // virtual обязательно!
}
Пример:
var user = db.Users.First(); // SELECT * FROM Users LIMIT 1
// Заказы загружаются только здесь (лениво):
var orders = user.Orders; // SELECT * FROM Orders WHERE UserId = 1
Жадная загрузка (Eager Loading) предварительно загружает связанные данные одним запросом с помощью метода .Include(). Данные загружаются сразу при выполнении основного запроса, и используется JOIN или дополнительные запросы (например, Include().ThenInclude()).
Мы явно говорим EF «Мне нужны пользователи и их заказы»:
var users = db.Users
.Include(u => u.Orders) // Жадно грузим заказы
.ToList();
EF делает один запрос с JOIN (или несколько запросов, но сразу).
В отличие от ленивой загрузки, здесь выполняется 1-2 запроса, а не сотни, таким образом, избавляясь от проблемы N+1. Но при таком подходе можно загрузить слишком много, потому что мы загрузили Orders целиком.
Запросил пользователя + заказы сразу → 1 запрос.
Пример:
var users = db.Users
.Include(u => u.Orders) // Жадная загрузка заказов
.ThenInclude(o => o.Items) // Загрузка товаров в заказах
.ToList();
SQL-эквивалент:
-- Вариант 1: JOIN (по умолчанию)
SELECT * FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
LEFT JOIN Items i ON o.Id = i.OrderId
-- Вариант 2: Отдельные запросы (если настроено в контексте)
SELECT * FROM Users;
SELECT * FROM Orders WHERE UserId IN (1, 2, ...);
SELECT * FROM Items WHERE OrderId IN (10, 20, ...);
Явная загрузка (Explicit Loading) помогает вручную загрузить связанные данные после загрузки основного объекта. Используются методы .Collection() для коллекций и .Reference() для одиночных объектов. Требует явного вызова .Load().
Мы сначала грузим пользователя:
var user = db.Users.First(); // SELECT * FROM Users LIMIT 1
А потом вручную говорим «Теперь загрузи его заказы»:
db.Entry(user)
.Collection(u => u.Orders)
.Load(); // SELECT * FROM Orders WHERE UserId = 1
В таком подходе мы получаем полный контроль - решаем сами, когда и что грузить, не делая лишних запросов, если данные не нужны. Минус - код длиннее (нужно писать .Load()).
Запросил пользователя → Потом решил загрузить заказы → 2 запроса.
Пример:
var user = db.Users.First(); // SELECT * FROM Users LIMIT 1
// Явная загрузка заказов:
db.Entry(user)
.Collection(u => u.Orders)
.Load(); // SELECT * FROM Orders WHERE UserId = 1
// Для одиночных связей (например, User.Profile):
db.Entry(user)
.Reference(u => u.Profile)
.Load();
Таким образом:
- ленивая загрузка загружает при первом обращении, имеет небольшой риск потери производительности, но зато автоматическая - и требует виртуальности свойств, подойдёт для простых сценариев вроде админок или прототипов;
- жадная загрузка загружает сразу в основном запросе, имеет оптимальную производительность, удобная для сложных запросов, подойдёт для API, когда важна производительность - ибо всё запрошенное выгрузится в память;
- явная загрузка загружает по требованию через Load(), производительность полностью в руках разработчика - ручное управление, подойдёт тогда, когда заранее не знаем, какие данные понадобятся.